<?php

namespace paymch\models;

use Yii;
use yii\base\Model;
use yii\db\Exception;
use yii\helpers\VarDumper;
use common\models\trade\Trade;
use common\models\trade\TradeOrder;
use common\models\trade\HandleTrade;
use common\models\original\UserAccountLogs;
use common\helpers\Utils;

/**
 * 统一下单
 * AlipayForm is the model behind the alipay async notify form.
 * 
 * @author emhome <emhome@163.com>
 * @since 2.0
 */
class UnifiedOrder extends Model {

    /**
     * 交易中心分配给商户的应用ID
     * @var string(16) 交易中心分配给商户的应用ID
     * @example 72300007148
     * @static 是
     */
    public $app_id;

    /**
     * 签名类型
     * @var string(10) 商户生成签名字符串所使用的签名算法类型，目前支持RSA2和RSA，推荐使用RSA2
     * @example RSA2
     * @static 是
     */
    public $sign_type;

    /**
     * 请求签名
     * @var string(256) 请参考异步返回结果的验签
     * @example 601510b7970e52cc63db0f44997cf70e
     * @static 是
     */
    public $sign;

    /**
     * 编码格式
     * @var string(10) 编码格式，如utf-8、gbk、gb2312等
     * @example utf-8
     * @static 是
     */
    public $charset;

    /**
     * 接口版本
     * @var string(3) 调用的接口版本，固定为：1.0
     * @example 1.0
     * @static 是
     */
    public $version;

    /**
     * 商家订单号
     * @var string(64) 外部商户订单号
     * @example 2013112011001004330000121536
     * @static 是
     */
    public $bizno;

    /**
     * 买家在APP的登录账号的UID
     * @var string(16) 对应的APP的唯一用户号
     * @example 2088102122524333
     * @static 是
     */
    public $buyer_id;

    /**
     * 交易使用的支付方式
     * @var string(16)  alipay|wechat|credit|apply
     * @example credit
     * @static 否
     */
    public $paymode;

    /**
     * 商户订单交易的总金额
     * @var decimal(9,2) 本次交易需支付的订单金额，单位为人民币（元）
     * @example 20
     * @static 是
     */
    public $total_amount;

    /**
     * 交易中选择使用的积分金额
     * @var string(10) 使用积分支付的金额，单位为积分单位（不使用积分为0）
     * @example 1200
     * @static 是
     */
    public $point_amount;

    /**
     * 商品订单标题
     * @var string(256) 商品的标题/交易标题/订单标题/订单关键字等，是请求时对应的参数
     * @example 商品A标题
     * @static 是
     */
    public $subject;

    /**
     * 商品订单描述
     * @var string(400) 该订单的备注、描述、明细等。对应请求时的body参数
     * @example 商品A内容
     * @static 否
     */
    public $body;

    /**
     * 交易过期时间
     * @var date 该笔交易过期时间。格式为 yyyyMMddHHmmss
     * @example 20150428154557
     * @static 是
     */
    public $expire_time;

    /**
     * 同步通知地址
     * @var string(256) 交易处理结束后返回对应的同步通知地址 http/https路径
     * @example 601510b7970e52cc63db0f44997cf70e
     * @static 否
     */
    public $return_url;

    /**
     * 回传参数
     * @var string(512) 公共回传参数，如果请求时传递了该参数，则返回给商户时会在异步通知时将该参数原样返回。本参数必须进行UrlEncode之后才可以发送给支付宝
     * @example merchantBizType%3d3C%26merchantBizNo%3d20160101011
     * @static 否
     */
    public $passback_params;
    private $_partner = null;
    private $_buyer = null;

    /**
     * 交易中心统一下单请求参数校验
     * @inheritdoc
     */
    public function rules() {
        return [
            [['sign_type', 'sign', 'charset', 'version', 'app_id', 'bizno', 'buyer_id', 'paymode', 'total_amount', 'point_amount', 'subject', 'expire_time'], 'required'],
            [['sign_type', 'charset', 'version', 'app_id'], 'string', 'max' => 16],
            ['bizno', 'string', 'max' => 32],
            ['buyer_id', 'integer'],
            ['paymode', 'in', 'range' => ['alipay', 'wechat', 'credit', 'apple']],
            [['total_amount', 'point_amount'], 'number'],
            ['subject', 'string', 'max' => 64],
            ['body', 'string', 'max' => 128],
            ['return_url', 'string', 'max' => 255],
            ['passback_params', 'string', 'max' => 512],
            ['expire_time', 'date', 'format' => 'yyyyMMddHHmmss'],
            //校验APPID
            ['app_id', 'validateAppId'],
            //验证下单请求签名
            ['sign', 'validateSignature'],
            //校验buyer_id
            ['buyer_id', 'validateBuyer'],
            //校验过期时间
            ['expire_time', 'validateExpireTime'],
            //校验总价格
            ['total_amount', 'compare', 'compareValue' => 0, 'operator' => '>'],
            //校验积分账户
            ['point_amount', 'validatePointAmount'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels() {
        return [
            'app_id' => '交易中心分配给商户的应用ID',
            'charset' => '编码格式',
            'version' => '接口版本',
            'sign_type' => '签名类型',
            'sign' => '签名',
            'bizno' => '商户业务号',
            'buyer_id' => '买家支付宝用户号',
            'paymode' => '支付方式',
            'total_amount' => '订单金额',
            'point_amount' => '支付积分',
            'subject' => '订单标题',
            'body' => '商品描述',
            'return_url' => '交易回调地址',
            'expire_time' => '交易过期时间',
            'passback_params' => '回传参数',
        ];
    }

    /**
     * Validates the APPID.
     * 验证app_id是否为该商户本身
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateAppId($attribute) {
        if (!$this->hasErrors()) {
            $partner = $this->getPartner();
            if (!$partner) {
                $this->addError($attribute, '未签约商户！');
            } else {
                $this->_partner = $partner;
            }
        }
    }

    /**
     * Validates the signature.
     * 校验请求的签名
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateSignature($attribute) {
        if (!$this->hasErrors()) {
            $paymchServer = new \common\components\paymch\MchServer([
                'appId' => $this->app_id,
                'version' => $this->version,
                'charset' => $this->charset,
                'rsaPublicKeyFilePath' => Yii::getAlias('@common') . '/data/keys/emchpay/rsa_public_key.pem',
            ]);
            $flag = $paymchServer->checkSignature($this->attributes, 'RSA2');
            if (!$flag) {
                $this->addError($attribute, '请求签名校验失败！');
            }
        }
    }

    /**
     * Validates the buyer_id.
     * 商户需要验证该通知数据中的buyer_id是否为商户系统中创建的订单号
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateBuyer($attribute) {
        if (!$this->hasErrors()) {
            $buyer = $this->getBuyer();
            if (!$buyer) {
                $this->addError($attribute, '非当前系统用户下单！');
            } else {
                $this->_buyer = $buyer;
            }
        }
    }

    /**
     * Validates the buyer_id.
     * 商户需要验证该通知数据中的buyer_id是否为商户系统中创建的订单号
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validateExpireTime($attribute) {
        if (!$this->hasErrors()) {
            if (strtotime($this->$attribute) < strtotime('+10 minute')) {
                $this->addError($attribute, '订单过期时间太短！');
            }
        }
    }

    /**
     * Validates the buyer_id.
     * 商户需要验证该通知数据中的buyer_id是否为商户系统中创建的订单号
     *
     * @param string $attribute the attribute currently being validated
     */
    public function validatePointAmount($attribute) {
        if (!$this->hasErrors()) {
            if ($this->total_amount * 10 < $this->$attribute) {
                $this->addError($attribute, '积分支付金额不能大于订单总金额！');
            } elseif (!$this->buyer->account || $this->buyer->account->credit < $this->$attribute) {
                $this->addError($attribute, '账户积分不足！');
            } elseif (($this->total_amount * 10 - $this->$attribute) > 0 && $this->paymode === Trade::PAYMODE_CREDIT) {
                $this->addError($attribute, '请选择正确的现金支付方式！');
            }
        }
    }

    /**
     * Find Partner by [[appid]]
     *
     * @return \common\models\trade\Partner|null
     */
    protected function getPartner() {
        if ($this->_partner === null) {
            $this->_partner = \common\models\trade\Partner::findByAppId($this->app_id);
        }
        return $this->_partner;
    }

    /**
     * Find Partner by [[appid]]
     *
     * @return \common\models\trade\Partner|null
     */
    protected function getBuyer() {
        if ($this->_buyer === null) {
            $this->_buyer = \common\models\original\Users::findOne($this->buyer_id);
        }
        return $this->_buyer;
    }

    /**
     * 
     * Sends an email to the specified email address using the information collected by this model.
     *
     * 在上述验证通过后商户必须根据支付宝不同类型的业务通知，正确的进行不同的业务处理，并且过滤重复的通知结果数据。
     * 在支付宝的业务通知中，只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时，支付宝才会认定为买家付款成功。
     * 
     * TRADE_FINISHED 	交易完成 	true（触发通知）
     * TRADE_SUCCESS 	支付成功 	true（触发通知）
     * WAIT_BUYER_PAY 	交易创建 	false（不触发通知）
     * TRADE_CLOSED 	交易关闭 	true（触发通知）
     * 
     * WAIT_BUYER_PAY 	交易创建，等待买家付款
     * TRADE_CLOSED 	未付款交易超时关闭，或支付完成后全额退款
     * TRADE_SUCCESS 	交易支付成功
     * TRADE_FINISHED 	交易结束，不可退款
     * 
     * @param string $email the target email address
     * @return bool whether the email was sent
     */
    public function save(&$msg) {
        if (!$this->validate()) {
            $msg = Utils::arrayToString($this->firstErrors, false);
            return false;
        }

        $this->expire_time = time() + Trade::AUTO_EXPIRED_TIME;

        //取消当前外部订单号下的所有历史交易
        if (HandleTrade::cancelByBizno($this->bizno, $msg) === false) {
            Yii::error('Bizno Order cancel error:' . $msg);
            return false;
        }

        //创建交易
        $trade = new Trade();
        $paymode = $this->paymode;
        $credit = $this->point_amount;
        $cash = $this->total_amount - $credit / 10;
        $tradeNo = $trade->generateTn($credit, $paymode, $this->partner->id);
        $tradeSuccess = true;

        //开启事务
        $transaction = Yii::$app->db->beginTransaction();
        try {
            $trade->attributes = [
                'user_id' => $this->buyer_id,
                'paymode' => $paymode,
                'total' => $this->total_amount,
                'cash' => $cash,
                'credit' => $credit / 10,
                'partner_id' => $this->partner->id,
                'bizno' => $this->bizno,
                'unified_no' => $this->bizno,
                'subject' => $this->partner->name . '订单' . $this->bizno,
                'body' => $this->body,
                'return_url' => $this->return_url,
                'expire_time' => $this->expire_time,
                'balance' => Trade::BALANCE_PAYMENT,
                'status' => Trade::STATUS_DEFAULT,
            ];
            if (!$trade->save()) {
                throw new Exception('交易创建失败！' . Utils::arrayToString($trade->errors, true));
            }

            if ($credit) {
                //扣除账户积分
                $account = $this->buyer->account;
                $account->credit -= $credit;
                $account->updated_at = date('Y-m-d H:i:s');
                if (!$account->save()) {
                    throw new Exception('账户积分支付失败！' . Utils::arrayToString($account->errors, true));
                }
                //创建积分交易
                $creditOrder = new TradeOrder();
                $creditOrder->attributes = [
                    'trade_id' => $trade->id,
                    'original_no' => $tradeNo,
                    'pay' => $credit / 10,
                    'expire_time' => $this->expire_time,
                    'paymode' => TradeOrder::PAYMODE_CREDIT,
                    'currency' => TradeOrder::CURRENCY_CREDIT,
                    'balance' => TradeOrder::BALANCE_PAYMENT,
                    'status' => TradeOrder::STATUS_DEFAULT,
                ];
                if (!$creditOrder->save()) {
                    throw new Exception('交易积分支付失败！' . Utils::arrayToString($creditOrder->errors, true));
                }
                //创建积分日志
                $creditLog = new UserAccountLogs();
                $creditLog->attributes = [
                    'user_id' => $this->buyer_id,
                    'type' => 'credit',
                    'amount' => -1 * $credit,
                    'current' => $account->credit,
                    'ip' => Yii::$app->request->userIP,
                    'mark' => '购买' . $this->subject,
                    'detail' => 'trade_order',
                    'detail_id' => $creditOrder->id,
                ];
                if (!$creditLog->save()) {
                    throw new Exception('积分日志更新失败！' . Utils::arrayToString($creditLog->errors, true));
                }
                //积分支付
                $creditOrder->pay_no = (string) $creditLog->id;
                $creditOrder->pay_time = $creditLog->input_at;
                $creditOrder->status = TradeOrder::STATUS_SUCCESS;
                if (!$creditOrder->save()) {
                    throw new Exception('交易积分支付单号保存失败！' . Utils::arrayToString($creditOrder->errors, true));
                }
            }

            if ($cash) {
                //创建现金交易
                $cashOrder = new TradeOrder();
                $cashOrder->attributes = [
                    'paymode' => $this->paymode,
                    'currency' => TradeOrder::CURRENCY_CNY,
                    'balance' => TradeOrder::BALANCE_PAYMENT,
                    'trade_id' => $trade->id,
                    'original_no' => $tradeNo,
                    'pay' => $cash,
                    'expire_time' => $this->expire_time,
                    'status' => TradeOrder::STATUS_DEFAULT,
                ];
                if (!$cashOrder->save()) {
                    throw new Exception('现金交易创建失败！' . Utils::arrayToString($cashOrder->errors, true));
                }
                $tradeSuccess = false;
            }

            if ($tradeSuccess === true) {
                $trade->status = Trade::STATUS_SUCCESS;
            }
            if (!$trade->save()) {
                throw new Exception('交易状态变更失败！' . Utils::arrayToString($trade->errors, true));
            }

            $transaction->commit();
        } catch (Exception $e) {
            $transaction->rollBack();
            $msg = $e->getMessage();
            Yii::error($msg . "\n" . VarDumper::export($this->attributes), 'payment');
            return false;
        }

        //创建自动过期任务队列
        $taskid = Yii::$app->queue->delay($this->expire_time - time())->push(new \common\jobs\TradeExpiredJob([
            'tradeid' => $trade->id
        ]));
        Yii::$app->commonCache->set('queue_trade_' . $trade->id, $taskid);

        return $trade->id;
    }

}
